Repository navigation
Ubshm transport fix 2 - #58
Open
Sherlock0203 wants to merge 11 commits into
Open
Sherlock0203 wants to merge 11 commits into
Sherlock0203 wants to merge 11 commits into
Conversation
Sherlock0203
force-pushed
the
ubshm_transport_fix_2
branch
from
September 24, 2026 07:31
72748e5 to
68a6914
Compare
…inQuickDev#57) Make the whole ubring shared-memory module follow the current brpc conventions, addressing review feedback: - Replace the module-local LIKELY/UNLIKELY aliases with BAIDU_LIKELY/ BAIDU_UNLIKELY and drop the aliases from common.h. - Use BAIDU_SCOPED_LOCK instead of the local GNU cleanup-attribute LOCK_GUARD. The pthread_mutex_t members keep their type, allocation and lifetime, so no static-init or placement-new change is needed. - Use butil::atomic instead of std::atomic. The timer handle slots and the trx cleanup slot are now real butil::atomic objects, so plain storage is never reinterpreted as an atomic: the timer API takes butil::atomic<UbrTimerId>* and UbrTrx/UbrCleanupCtl hold atomic handles. UbrTrx slots are value-initialized with placement new so those atomics are constructed, replacing the per-acquisition memset. - Drop the two per-node INFO logs on the shm cleanup retry path in UbsShmCallback; DeleteShmToList still records each drained node. No behavioral change: the removed aliases expanded 1:1, BAIDU_SCOPED_LOCK locks the same pthread_mutex_t, butil::atomic is layout-identical to std::atomic, and placement-new value-initialization zeroes exactly what memset zeroed while also constructing the atomic members.
Sherlock0203
force-pushed
the
ubshm_transport_fix_2
branch
2 times, most recently
from
September 29, 2026 06:57
14450e2 to
48e403b
Compare
Address the ubring lifecycle review on the bthread-timer refactor. Generation validation: - UbrTimerStart/UbrTimerTask carry an opaque `gen' and pass it back to the callback on every fire, so a callback armed for an object that was later released and reused can detect that it is stale. `gen' is required and sits before the optional backoff; the UBS shm timer passes 0 explicitly. - UbrScheduleClearTimer takes expect_ubr_id, rejects a mismatch before creating the cleanup ctl, and stamps ctl->ubr_id with it instead of reading the slot's current generation. - UbrTrxCallbackCheck runs before the generation check in the close and heartbeat callbacks, so trx is null-checked before trx->ubr_id is dereferenced; UbrTrxClose and UbEventCallback pass the generation they observed for the slot. - UbrPassiveClearTrx/ClearTrxResource/UbrAddAsynClearTimer thread expect_ubr_id through. Teardown: - ReleaseUbrTrxFromMgr disarms the per-acquisition close/heartbeat timers after the generation check and before the slot is marked FREE, so a rollback no longer orphans a periodic task onto the slot's next occupant. - UbrMgrFini snapshots the USED slots under g_ubr_trx_mgr_mtx and UbrTimerDelAndWait's their timers outside the lock before the pool is freed; the previous loop only waited for cleanup-ctl callbacks, so a dispatched close/heartbeat callback could still be reading UbrTrx when FREE_PTR(trx_mgr) ran. The walk is guarded by "all four pool arrays are allocated", which also stops the ctl loops from reading arrays that UbrMgrInit's allocation-failure path leaves uninitialized. - UbrClearResourceCheck/UbrPassiveClearTrx stop the per-trx timers through UbrStopTrxTimer, which waits with UbrTimerDelAndWait unless the caller is the timer callback itself (in_timer_callback), so teardown from a user thread or from the ubsmem fault callback cannot clear a trx whose callback is mid-flight. UbrTrxCloseCallback re-validates its event queue pointers after the generation check, like UbrTrxHBCallback, so a concurrent release cannot turn the dereference into a null access. The ordering argument is a synchronization one: bthread dispatches every timer callback from a single global timer thread, so two per-trx callbacks never run concurrently and the one-shot delayed-clear callback is ordered after them; ub_flying_io_timeout_s only delays the clear so in-flight IO can drain. Non-blocking UbrTimerDel is kept on the paths that hold g_ubr_trx_mgr_mtx (ReleaseUbrTrxFromMgr and the callback side), where the callbacks take the same lock and a wait would deadlock. Tested: cmake --build cmake-build-debug --target brpc_ubring_unittest, and ./test/brpc_ubring_unittest (12/12 passed).
Address the second ubring review round on the bthread-timer refactor. Timer facade: - A periodic callback that deletes its own timer now only marks the task stopped and leaves it anchored in the handle slot until the callback returns; UbrTimerOnFire retires the slot on exit. Previously the callback pulled the task out of the slot first, so a concurrent UbrTimerDelAndWait returned immediately and the caller could clear a trx whose callback was still running. - UbrTimerDelAndWait recognizes that the caller is the callback of that very timer and degrades to the non-blocking delete, which removes the in_timer_callback argument that every teardown entry point had to thread correctly (and could get wrong). - UbrTimerStart (one-shot) and UbrTimerStartPeriodic are now separate functions, and a periodic start rejects interval_us == 0 instead of silently arming a one-shot: that dropped the close check, the heartbeat or the shm drain for the rest of the connection's life. The close and hb call sites use the periodic form and validate their flags; the shm drain period is derived to stay positive because its flag legitimately accepts 0 for the one-shot delayed-clear. - A back-off function returning 0 keeps the previous interval: re-arming for "now" would let one timer monopolize the process-wide timer thread. - The two join waits poll every 100us instead of 1ms; they only run on teardown paths. Cleanup-control publication and trx close: - UBRingManager::TryClaimTrxClose validates the slot generation and moves close_cnt in one critical section, so a close started from a stale snapshot cannot mark the slot's new occupant CLOSED or delete its timers. UbEventCallback looks the faulty link up before it closes it, so this was reachable through the ubsmem fault callback; UbrTrxCloseCheck takes expect_ubr_id and is now only a wrapper around the claim. - TryPublishUnitCleanupCtl publishes the cleanup control object on the trx and anchors it in the pool slot under one lock, and DetachUnitCleanupCtl clears both, so a force close never observes a trx-side publication without its anchor. The inline fallback gates the cleanup on IsUbrTrxSlotUsed instead of ubr_id: ReleaseUbrTrxFromMgr does not change ubr_id, so the old check let a released slot be cleaned twice. Shutdown: - UbrMgrFini takes a shutdown barrier before anything else: acquisitions, timer arming (ArmTimersExclusive, now used by UbrAddTimer) and cleanup publications are refused, and the faulty-shm callback registers through BeginPoolAccess, which Fini waits for before the pool is freed. Because arming is serialized by the same lock, the timers armed before the flag are the complete set and one wait pass replaces the retry loop whose window was never actually closed. - The shm list is torn down through a handle: DestroyShmTimer takes ShmList** and clears it, so g_shm_list is never left dangling, a repeated teardown is a no-op and UbsShmFini is idempotent, which makes a repeated ShmMgrFini safe instead of a double free. UbrMgrInit value-initializes the pool instead of memset'ing it, matching the placement-new UbrTrx construction the style commit introduced (UbrTrx holds butil::atomic members). Tested: cmake --build cmake-build-debug --target brpc_ubring_unittest, and ./test/brpc_ubring_unittest (12/12 passed).
Sherlock0203
force-pushed
the
ubshm_transport_fix_2
branch
from
September 30, 2026 08:16
48e403b to
5f55f69
Compare
…inQuickDev#62) Address the second ubring review round on the bthread-timer refactor. Timer facade: - A periodic callback that deletes its own timer now only marks the task stopped and leaves it anchored in the handle slot until the callback returns; UbrTimerOnFire retires the slot on exit. Previously the callback pulled the task out of the slot first, so a concurrent UbrTimerDelAndWait returned immediately and the caller could clear a trx whose callback was still running. - UbrTimerDelAndWait recognizes that the caller is the callback of that very timer and degrades to the non-blocking delete, which removes the in_timer_callback argument that every teardown entry point had to thread correctly (and could get wrong). - UbrTimerStart (one-shot) and UbrTimerStartPeriodic are now separate functions, and a periodic start rejects interval_us == 0 instead of silently arming a one-shot: that dropped the close check, the heartbeat or the shm drain for the rest of the connection's life. The close and hb call sites use the periodic form and validate their flags; the shm drain period is derived to stay positive because its flag legitimately accepts 0 for the one-shot delayed-clear. - A back-off function returning 0 keeps the previous interval: re-arming for "now" would let one timer monopolize the process-wide timer thread. - The two join waits poll every 100us instead of 1ms; they only run on teardown paths. Cleanup-control publication and trx close: - UBRingManager::TryClaimTrxClose validates the slot generation and moves close_cnt in one critical section, so a close started from a stale snapshot cannot mark the slot's new occupant CLOSED or delete its timers. UbEventCallback looks the faulty link up before it closes it, so this was reachable through the ubsmem fault callback; UbrTrxCloseCheck takes expect_ubr_id and is now only a wrapper around the claim. - TryPublishUnitCleanupCtl publishes the cleanup control object on the trx and anchors it in the pool slot under one lock, and DetachUnitCleanupCtl clears both, so a force close never observes a trx-side publication without its anchor. The inline fallback gates the cleanup on IsUbrTrxSlotUsed instead of ubr_id: ReleaseUbrTrxFromMgr does not change ubr_id, so the old check let a released slot be cleaned twice. Shutdown: - UbrMgrFini takes a shutdown barrier before anything else: acquisitions, timer arming (ArmTimersExclusive, now used by UbrAddTimer) and cleanup publications are refused, and the faulty-shm callback registers through BeginPoolAccess, which Fini waits for before the pool is freed. Because arming is serialized by the same lock, the timers armed before the flag are the complete set and one wait pass replaces the retry loop whose window was never actually closed. - The shm list is torn down through a handle: DestroyShmTimer takes ShmList** and clears it, so g_shm_list is never left dangling, a repeated teardown is a no-op and UbsShmFini is idempotent, which makes a repeated ShmMgrFini safe instead of a double free. UbrMgrInit value-initializes the pool instead of memset'ing it, matching the placement-new UbrTrx construction the style commit introduced (UbrTrx holds butil::atomic members). Tested: cmake --build cmake-build-debug --target brpc_ubring_unittest, and ./test/brpc_ubring_unittest (12/12 passed).
…Dev#63) UbrTrxClose loaded _trx->ubr_id before calling UbrTrxCloseCheck, which is where the null check lived, so that guard became unreachable and the load dereferenced null. The path is reachable: UbrAllocateLocalShm and UbrAllocateServerShm reset _trx to nullptr on their error paths (a failing shm map, acquire or trx init) while the endpoint keeps the non-null _ub_ring, and UBShmEndpoint::DeallocateResources calls UbrTrxClose unconditionally from Reset(), so a connection whose setup failed crashes when its endpoint is destroyed, or when it falls back to TCP. Check _trx first and return UBRING_ERR, which restores the behavior from before the generation argument was added; DeallocateResources ignores the return value. UbrAddTimer takes the same guard: its failure branch reports _trx->local_shm.name before the guards in UbrAddCloseTimer/UbrAddHBTimer can reject a null trx. UbrTrxCloseCheck keeps its own null check: UbrPassiveClearTrx still routes through it. Tested: cmake --build cmake-build-debug --target brpc_ubring_unittest, and ./test/brpc_ubring_unittest (12/12 passed).
Sherlock0203
force-pushed
the
ubshm_transport_fix_2
branch
from
October 9, 2026 08:18
a345b29 to
e47d004
Compare
* fix(ubshm): retire the slot storage before a deleted one-shot timer returns UbrTimerDel wins the handle-slot exchange and reports success for a one-shot timer, but a wrapper that the timer thread already dispatched may still be between its dispatch and its own (losing) slot exchange. The caller of UbrTimerDel treats return 0 as "the callback will not run" and frees the object that stores the slot -- UbrCleanupCtl on the delayed-clear path -- so that late wrapper access is a use-after-free. Track the slot retirement on the task: the one-shot wrapper sets slot_retired right after its slot CAS and before running the user callback, and UbrTimerDel waits for it when bthread_timer_del reports the task as dispatched. Callers that win the cancellation get the same guarantee from the delete itself. The wait only spins on a task-internal flag, so it is safe to do under the trx manager lock that the force-close, pool-acquire and UbrMgrFini callers hold, and it is limited to the one-shot path (a periodic task's slot storage is pool memory that the non-blocking callers never free). * fix(ubshm): claim forced cleanup and cleanup publication atomically The force close used to snapshot the pool slot's cleanup control object under the manager lock and then re-verify "still no ctl" out of the lock. A concurrent SDK-fault callback could publish a delayed cleanup for the same generation in between, so both the force close and the delayed-clear callback would run the cleanup of the same shared memory (double unmap / stale event queues). Replace the two steps with ClaimTrxCleanupForced, which snapshots the ctl and, when there is none, claims the cleanup for the generation inside one critical section, and make TryPublishUnitCleanupCtl refuse to publish once that claim happened (new per-acquisition trx->cleanup_forced flag, reset by the next AcquireUbrTrxFromMgr). Publication and claim are now totally ordered by the manager lock, so exactly one of them owns the cleanup. An existing publication keeps the current ctl->state arbitration untouched. * fix(ubshm): offload ubs sdk cleanup from the global timer thread Timers are bthread based, so every callback in the process runs on one global timer thread. UbsShmCallback and the delayed-clear callbacks still called the UBS SDK there (ubsmem_shmem_unmap/deallocate), and those calls can wait on the SDK daemon with no latency bound, so a slow or failed shm release delayed every other timer in the process -- RPC timeouts, backup requests, bthread_usleep wakeups and ubring's own heartbeat/close checks. Add a lazily started cleanup worker bthread with an MPSC job queue. The timer callbacks now only arbitrate cleanup ownership and post a job (shared-memory drain or trx cleanup); the worker runs the blocking part, and takes over the timer/callback reference of the cleanup control object. Callers run the work inline only when the worker cannot be started. The drain keeps the previous retry semantics (the node stays in the list on unmap failure and the periodic timer retries it) and at most one drain step is in flight, and DestroyShmTimer drains the worker before it frees the list and finalizes the SDK. UbrMgrFini already waits for the control-object references, so it needs no change. * fix(ubshm): serialize cleanup worker posting with the lazy start Post() was not serialized with the lazy start of the worker bthread. Two posters could decide "the worker is started" and "enqueue the job" independently: one thread won the start CAS and was about to see bthread_start_background fail, while another read g_worker_started == true and enqueued a job. Since g_worker_unavailable is terminal and no worker ever runs, that job is never dequeued -- its UbrCleanupCtl reference leaks, the pool slot is never released, and g_cleanup_inflight never returns to zero, so every later UbrCleanupWorker::DrainAndWait (DestroyShmTimer on a teardown path) hangs forever. Take a small mutex in Post() around the unavailable check, the start attempt and the enqueue, so a job can only ever be queued behind a worker that really started. Posting only happens on cleanup paths, the worker never takes the mutex, and a failed start now returns false (callers keep running the work inline) without having enqueued anything. * style(ubshm): prefix the cleanup claim results with UBR_CLEANUP_CLAIM The UbrCleanupClaim members were the only constants in ub_ring_manager.h without the module's UBR_ prefix, next to UBR_MGR_UNIT_* and UBR_CLEANUP_*. Rename them to UBR_CLEANUP_CLAIM_NOT_OURS / _HAS_CTL / _OWNED_NULL. * docs(ubshm): document the cleanup worker and the one-shot delete contract The Timer Management sections still implied all cleanup work runs on the timer thread. Document that the delayed-clear and pending-unmap callbacks only arbitrate ownership and post a job to the cleanup worker, and that a one-shot UbrTimerDel returning 0 now also guarantees the facade is done with the storage the handle slot lives in, so the caller may free the object holding it (UbrCleanupCtl on the delayed-clear path). Also refresh the ordering comment above UbrTrxCloseCallback: with the cleanup body on the worker, the guarantee rests on the generation gate the worker re-applies under the manager lock (IsUbrTrxSlotUsed) plus the per-trx UbrStopTrxTimer/UbrTimerDelAndWait waits, not on the timer thread serializing the cleanup itself.
g_cleanup_queue was a file-scope MPSCQueue with a destructor, while the cleanup worker is a bthread that is never stopped: the bthread runtime itself is not destructed at exit (see the note in task_control.cpp), so the worker can still be inside Dequeue() when static destructors run. MPSCQueue's destructor drains the queue, turning that destruction into a second concurrent consumer of a single-consumer queue: _cur_dequeue_node is a plain pointer, so the destructor and the worker could pick and free the same node (double free) or interleave their advances (lost nodes). A timer callback firing during the destructor phase would additionally enqueue into a destroyed queue. No orderly teardown drains the queue at exit -- GlobalRelease only runs on init failure and in UT teardown -- so the residue the destructor would drain is really possible. Allocate the queue with new and never delete it, following the brpc convention for globals entangled with threads that never stop (g_task_control itself); the kernel reclaims it at exit. DrainAndWait observes g_cleanup_inflight, not the queue, so teardown semantics are unchanged. g_cleanup_inflight (trivially destructible atomics) and g_worker_mtx (a static initializer, nothing to destroy) keep their storage. Tested: cmake --build build --target brpc_ubring_unittest, and ./build/test/brpc_ubring_unittest (12/12 passed).
A failed bthread_start_background in Post() latched g_worker_unavailable forever, so every later cleanup fell back to running inline: the SDK calls went back to the process-wide timer thread (UbsShmDrainNode from UbsShmCallback, the delayed trx cleanups from their callbacks), delaying unrelated RPC timeouts, backup requests and bthread wakeups -- exactly what the worker exists to avoid. bthread creation fails transiently (ENOMEM), so the degradation correlated with memory pressure, the moment where blocking the timer thread hurts most, and it never self-healed. Drop the terminal flag and retry the lazy start on every post made while the worker is not started. A failed attempt leaves g_worker_started false and fails only that post; the caller runs it inline as the safety net, and the next post retries the start. The posting mutex already serializes the start attempt with the enqueue, so a job is still only ever queued behind a start that succeeded -- the flag was redundant double insurance for orphaned jobs. The error is logged with LOG_EVERY_SECOND so a failure window cannot spam. Add the g_ub_test_worker_start_rc hook so the new UT can inject a start failure deterministically (no real bthread is created while it is set), and cover both review cases: repeated failures fail only their own post and queue nothing, and the first post after the injection is cleared retries the start, runs the job exactly once and lets DrainAndWait observe quiescence. The suite must observe the worker in its unstarted state, so it is declared ahead of UBShmEndpointTest, whose teardown posts cleanups, and probes for an already-started worker to skip rather than assert on a start that is never attempted. Tested: cmake --build build --target brpc_ubring_unittest, and ./build/test/brpc_ubring_unittest (13/13 passed). Reintroducing the latch locally makes the new UT fail, confirming it guards the retry.
Sherlock0203
force-pushed
the
ubshm_transport_fix_2
branch
from
October 12, 2026 08:52
cf25cd6 to
6befc10
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.